package com.cmc.cloud.cmclink.doc.service.impl.si;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

import javax.annotation.Resource;

import org.apache.commons.collections.CollectionUtils;
import org.apache.tika.utils.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.cmc.cloud.cmclink.doc.api.document.emums.*;
import com.cmc.cloud.cmclink.doc.constants.BlnoFreightConstants;
import com.cmc.cloud.cmclink.doc.constants.CommonConstants;
import com.cmc.cloud.cmclink.doc.constants.RedisKeyConstants;
import com.cmc.cloud.cmclink.doc.constants.errorconstant.BlnoFreightErrorCodeConstants;
import com.cmc.cloud.cmclink.doc.convert.*;
import com.cmc.cloud.cmclink.doc.dto.CalFrtRateUnitRespDTO;
import com.cmc.cloud.cmclink.doc.dto.CalFrtRespDTO;
import com.cmc.cloud.cmclink.doc.dto.ImExVslvoyAge;
import com.cmc.cloud.cmclink.doc.entity.*;
import com.cmc.cloud.cmclink.doc.mapper.BlnoFreightMapper;
import com.cmc.cloud.cmclink.doc.mapper.BlnoMapper;
import com.cmc.cloud.cmclink.doc.mapper.ConfirmNoticeFcnMapper;
import com.cmc.cloud.cmclink.doc.mapper.ConfirmNoticeMapper;
import com.cmc.cloud.cmclink.doc.rpcservice.AccountTransfer;
import com.cmc.cloud.cmclink.doc.rpcservice.CustomerRpcService;
import com.cmc.cloud.cmclink.doc.rpcservice.RateTransfer;
import com.cmc.cloud.cmclink.doc.rpcservice.VesselTransferService;
import com.cmc.cloud.cmclink.doc.service.BaseCodeService;
import com.cmc.cloud.cmclink.doc.service.BlnoCntrService;
import com.cmc.cloud.cmclink.doc.service.BlnoFreightForecastService;
import com.cmc.cloud.cmclink.doc.service.si.*;
import com.cmc.cloud.cmclink.doc.util.*;
import com.cmc.cloud.cmclink.doc.util.sepcial.CommonUtil;
import com.cmc.cloud.cmclink.doc.vo.blnocntrvo.BlnoCntrRespVO;
import com.cmc.cloud.cmclink.doc.vo.blnofreightvo.*;
import com.cmc.cloud.cmclink.doc.vo.calculatelogvo.CalculateOperateLogCreateReqVO;
import com.cmc.cloud.cmclink.fin.api.base.dto.req.DocBlnoBaseReqDTO;
import com.cmc.cloud.cmclink.fin.api.base.dto.req.DocBlnoCreateReqDTO;
import com.cmc.cloud.cmclink.fin.api.base.dto.req.DocBlnoFreightInfoReqDTO;
import com.cmc.cloud.cmclink.fin.api.base.dto.req.DocBlnoVslvoyReqDTO;
import com.cmc.cloud.cmclink.mkt.dto.rate.*;
import com.cmc.cloud.cmclink.svl.api.base.resp.VesselVoyRespDTO;
import com.cmes.framework.common.exception.util.ServiceExceptionUtil;
import com.cmes.framework.ex.Assertion;
import com.cmes.framework.mybatis.core.query.LambdaQueryWrapperX;
import com.cmes.framework.security.core.util.SecurityFrameworkUtils;
import com.google.gson.Gson;

import cn.hutool.core.util.IdUtil;
import lombok.extern.slf4j.Slf4j;

/**
 * 海运费附加费 Service 实现类
 *
 * @author 集运管理员
 */
@Service
@Validated
@Slf4j
public class BlnoFreightServiceImpl implements BlnoFreightService {

    private static final BigDecimal MIN_WEIGHT_ONE = new BigDecimal("1");
    private static final BigDecimal MIN_WEIGHT_TWO = new BigDecimal("2");
    private static final BigDecimal MIN_WEIGHT_THREE = new BigDecimal("3");
    private static final String RATE_PAYMENT_BILL = "BILL";
    private static final String RATE_PAYMENT_CBM = "CBM";

    private static final String MSG = "msg";

    private static final String GET_RATE_NULL = "获取营销费率为null";

    private static final String GET_RATE_CODE_ERROR = "获取营销费率返回码不为0， code:";

    @Resource
    private BlnoFreightMapper blnoFreightMapper;
    @Resource
    private BlnoService blnoService;
    @Resource
    private RateTransfer rateTransfer;
    @Resource
    private BlnoCntrService blnoCntrService;
    @Resource
    private BlnoCargoService blnoCargoService;
    @Resource
    private CalculateOperateLogService calLogService;
    @Resource
    private VesselTransferService vesselTransferService;
    @Resource
    private BlnoVslvoyService blnoVslvoyService;
    @Resource
    private BlnoScnService blnoScnService;
    @Resource
    private BaseCodeService baseCodeService;
    @Resource
    private BlnoFreightForecastService freightForecastService;
    @Resource
    private BlnoFreightDetailService freightDetailService;
    @Resource
    private AccountTransfer accountTransfer;
    @Resource
    private BlnoFreightForecastService blnoFreightForecastService;
    @Resource
    private BlnoFreightDetailService blnoFreightDetailService;
    @Resource
    private BlnoPaymentService blnoPaymentService;
    @Resource
    private CustomerRpcService nameTransferService;
    @Resource
    private BlnoMapper blnoMapper;
    @Value("${freight.redisTimeOut}")
    private Long redisTimeOut;
    @Resource
    private RedisUtils redisUtils;
    @Resource
    private CostChargeUtil costChargeUtil;

    @Resource
    private ConfirmNoticeFcnMapper confirmNoticeFcnMapper;

    @Resource
    private ConfirmNoticeMapper confirmNoticeMapper;

    @Resource
    private VoyageLockService voyageLockService;

    @Resource
    private OogUtil oogUtil;

    /**
     * 构造vslvoy参数
     *
     * @param blnoVslvoyDOList vslvoy
     * @return java.util.List<com.cmc.cloud.cmclink.fin.api.base.dto.req.DocBlnoVslvoyReqDTO>
     */
    public static List<DocBlnoVslvoyReqDTO> buildVslvoyReq(List<BlnoVslvoyDO> blnoVslvoyDOList) {
        List<DocBlnoVslvoyReqDTO> vslvoyReqList = new ArrayList<>();
        blnoVslvoyDOList.forEach(vslvoy -> {
            DocBlnoVslvoyReqDTO vslvoyReq = BlnoFreightConvert.INSTANCE.convertVslvoyReq(vslvoy);
            vslvoyReqList.add(vslvoyReq);
        });
        return vslvoyReqList;
    }

    @Override
    public void createBlnoFreight(BlnoFreightCreateReqVO createReqVO) {
        // 插入
        BlnoFreightDO blnoFreight = BlnoFreightConvert.INSTANCE.convert(createReqVO);
        blnoFreightMapper.insert(blnoFreight);
    }

    @Override
    public void updateBlnoFreight(BlnoFreightUpdateReqVO updateReqVO) {
        // 校验存在
        validateBlnoFreightExists(updateReqVO.getId());
        // 更新
        BlnoFreightDO updateObj = BlnoFreightConvert.INSTANCE.convert(updateReqVO);
        blnoFreightMapper.updateById(updateObj);
    }

    @Override
    public void deleteBlnoFreight(Long id) {
        // 校验存在
        validateBlnoFreightExists(id);
        // 删除
        blnoFreightMapper.deleteById(id);
    }

    private void validateBlnoFreightExists(Long id) {
        if (blnoFreightMapper.selectById(id) == null) {
            throw ServiceExceptionUtil.exception(BlnoFreightErrorCodeConstants.BLNO_FREIGHT_NOT_EXISTS);
        }
    }

    @Override
    public BlnoFreightDO getBlnoFreight(Long id) {
        return blnoFreightMapper.selectById(id);
    }

    @Override
    public BlnoFreightAllRespVO getBlnoAllFreight(Long id) {
        List<BlnoFreightDO> freightDOList = getBlnoFreightByBlnoId(id);
        List<BlnoFreightForecastDO> freightForecastDOList =
            blnoFreightForecastService.getBlnoFreightForecastByBlonId(id);
        List<BlnoFreightDetailDO> freightDetailDOList = blnoFreightDetailService.getBlnoFreightDetailByBlonId(id);
        List<BlnoCntrDO> blnoCntrDOList = blnoCntrService.getBlnoCntrByBlnoId(id);
        List<BlnoCntrRespVO> blnoCntrRespList = BlnoCntrConvert.INSTANCE.convertList(blnoCntrDOList);
        BlnoDO blnoDO = blnoService.getBlno(id);
        blnoCntrRespList.forEach(cntr -> cntr.setCntrStatus(blnoDO.getCntrStatus()));
        CalculateOperateLogDO logDO =
            calLogService.getNewOperateLogByBlnoId(id, CalculateTypeEnum.CALCULATE_FORECAST_FREIGHT);

        // 获取供应商信息
        Set<String> codes = freightDOList.stream().map(BlnoFreightDO::getPayer).filter(Objects::nonNull)
            // 过滤null
            .filter(name -> !name.isEmpty()).collect(Collectors.toSet());
        Map<String, String> companyNameMap = nameTransferService.getCompanyNameMap(new ArrayList<>(codes));
        List<BlnoFreightRespVO> freightRespList = BlnoFreightConvert.INSTANCE.convertList(freightDOList);
        List<BlnoFreightDetailRespVO> freightDetailRespList =
            BlnoFreightDetailConvert.INSTANCE.convertList(freightDetailDOList);
        freightRespList.forEach(freight -> freight.setPayNameEn(companyNameMap.getOrDefault(freight.getPayer(), null)));
        freightDetailRespList
            .forEach(freight -> freight.setPayNameEn(companyNameMap.getOrDefault(freight.getPayer(), null)));

        BlnoFreightAllRespVO respVO = new BlnoFreightAllRespVO();
        respVO.setBlnoFreightForecastList(BlnoFreightForecastConvert.INSTANCE.convertList(freightForecastDOList));
        respVO.setBlnoFreightSummaryList(freightRespList);
        respVO.setBlnoFreightClpList(freightDetailRespList);
        respVO.setBlnoCntrList(blnoCntrRespList);
        if (logDO != null) {
            respVO.setSendJson(logDO.getSendJson());
            respVO.setReceiveJson(logDO.getReceiveJson());
        }
        return respVO;
    }

    @Override
    public List<BlnoFreightDO> getBlnoFreightList(Collection<Long> ids) {
        return blnoFreightMapper.selectBatchIds(ids);
    }

    @Override
    public List<BlnoFreightDO> getBlnoFreightByBlnoIdList(Collection<Long> blnoIds) {
        LambdaQueryWrapperX<BlnoFreightDO> queryWrapperX = new LambdaQueryWrapperX<>();
        queryWrapperX.in(BlnoFreightDO::getBlnoId, blnoIds);
        return blnoFreightMapper.selectList(queryWrapperX);
    }

    @Override
    public List<BlnoFreightDO> getBlnoFreightByBlnoId(Long blnoId) {
        LambdaQueryWrapper<BlnoFreightDO> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(BlnoFreightDO::getBlnoId, blnoId);
        queryWrapper.orderByAsc(BlnoFreightDO::getChargeCode).orderByAsc(BlnoFreightDO::getId);
        return blnoFreightMapper.selectList(queryWrapper);
    }

    @Override
    public List<BlnoFreightCalRespVO> getBlnoWithFreightList(BlnoFreightCalReqVO reqVO) {
        checkParamValid(reqVO);
        List<BlnoDO> blnoDOList = blnoService.getBlnoWithFreightList(reqVO);
        if (CollectionUtils.isEmpty(blnoDOList)) {
            return new ArrayList<>();
        }

        List<BlnoFreightCalRespVO> respList = BlnoFreightConvert.INSTANCE.convertFreightList(blnoDOList);
        // 获取运输条款name
        Set<String> shipCodes = respList.stream().map(BlnoFreightCalRespVO::getShippingTerm).filter(Objects::nonNull)
            .filter(name -> !name.isEmpty()).collect(Collectors.toSet());
        Map<String, String> shipTermNameMap = baseCodeService.getShipCodeNameEn(new ArrayList<>(shipCodes));

        // 获取计算数量相关信息
        List<Long> ids = respList.stream().map(BlnoFreightCalRespVO::getId).collect(Collectors.toList());
        List<BlnoFreightCalInfoVO> calInfoList = blnoFreightMapper.getBlnoFreightCalInfoList(ids);
        Map<Long, BlnoFreightCalInfoVO> calInfoMap =
            calInfoList.stream().collect(Collectors.toMap(BlnoFreightCalInfoVO::getId, Function.identity()));

        // 获取供应商信息
        Set<String> codes = respList.stream().map(BlnoFreightCalRespVO::getBkpCode).filter(Objects::nonNull)
            .filter(name -> !name.isEmpty()).collect(Collectors.toSet());
        Map<String, String> companyNameMap = nameTransferService.getCompanyNameMap(new ArrayList<>(codes));
        respList.forEach(resp -> {
            resp.setBkpNameEn(companyNameMap.getOrDefault(resp.getBkpCode(), null));
            resp.setShippingTermName(shipTermNameMap.getOrDefault(resp.getShippingTerm(), null));
            BlnoFreightCalInfoVO calInfo = calInfoMap.get(resp.getId());
            resp.setToAccount(calInfo.getToAccount());
            resp.setToAccountAll(calInfo.getToAccountAll());
            resp.setCalForecastFlag(calInfo.getCalForecastCount() != null && calInfo.getCalForecastCount() != 0);
            resp.setCalOceanFlag(calInfo.getToAccountAll() != null && calInfo.getToAccountAll() != 0);
        });

        List<String> calOceanStatuses = reqVO.getCalOceanStatuses();
        if (!CollectionUtils.isEmpty(calOceanStatuses) && calOceanStatuses.size() == 1) {
            String calOceanStatus = calOceanStatuses.get(0);
            if (calOceanStatus.equalsIgnoreCase("Y")) {
                respList =
                    respList.stream().filter(resp -> resp.getCalOceanFlag() == true).collect(Collectors.toList());
            } else {
                respList =
                    respList.stream().filter(resp -> resp.getCalOceanFlag() == false).collect(Collectors.toList());
            }
        }
        return respList;
    }

    @Override
    public List<MultiBlnoFreightRespVO> getMultiBlnoInfo(MultiBlnoFreightReqVO reqVO) {
        checkParamValid(reqVO);
        List<BlnoDO> blnoDOList = getMultiBlnoWithFreightList(reqVO);
        if (CollectionUtils.isEmpty(blnoDOList)) {
            return new ArrayList<>();
        }

        List<MultiBlnoFreightRespVO> respList = BlnoFreightConvert.INSTANCE.convertMultiFreightList(blnoDOList);
        // 获取运输条款name
        Set<String> shipCodes = respList.stream().map(MultiBlnoFreightRespVO::getShippingTerm).filter(Objects::nonNull)
            .filter(name -> !name.isEmpty()).collect(Collectors.toSet());
        Map<String, String> shipTermNameMap = baseCodeService.getShipCodeNameEn(new ArrayList<>(shipCodes));

        // 获取供应商信息
        Set<String> codes = respList.stream().map(MultiBlnoFreightRespVO::getBkpCode).filter(Objects::nonNull)
            .filter(name -> !name.isEmpty()).collect(Collectors.toSet());
        Map<String, String> companyNameMap = nameTransferService.getCompanyNameMap(new ArrayList<>(codes));
        respList.forEach(resp -> {
            resp.setBkpNameEn(companyNameMap.getOrDefault(resp.getBkpCode(), null));
            resp.setShippingTermName(shipTermNameMap.getOrDefault(resp.getShippingTerm(), null));
        });

        return respList;
    }

    private List<BlnoDO> getMultiBlnoWithFreightList(MultiBlnoFreightReqVO reqVO) {
        List<String> rejectStatuList = new ArrayList<>();
        rejectStatuList.add(SiStatusEnum.BookingDraft.getValue());
        rejectStatuList.add(SiStatusEnum.BookingProcessing.getValue());

        LambdaQueryWrapperX<BlnoDO> queryWrapperX = new LambdaQueryWrapperX<>();
        queryWrapperX.eqIfPresent(BlnoDO::getCurrentPort, reqVO.getTsPort())
            .eqIfPresent(BlnoDO::getTopAgentCode, reqVO.getTopAgentCode())
            .eqIfPresent(BlnoDO::getBkgVesselCode, reqVO.getBkgVesselCode())
            .eq(BlnoDO::getValid, SiValidEnum.VALID.getValue()).eqIfPresent(BlnoDO::getBkgVoyage, reqVO.getBkgVoyage())
            .eqIfPresent(BlnoDO::getBlNo, reqVO.getBlNo()).eqIfPresent(BlnoDO::getSpaceOwner, reqVO.getSpaceOwner())
            .eqIfPresent(BlnoDO::getBkpCode, reqVO.getBkpCode())
            .eqIfPresent(BlnoDO::getAgreementNo, reqVO.getAgreementNo()).notIn(BlnoDO::getSiStatus, rejectStatuList)
            .eq(BlnoDO::getPodBlFlag, true).apply("por_code != pol_code");
        return blnoMapper.selectList(queryWrapperX);
    }

    private void checkParamValid(BlnoFreightCalReqVO reqVO) {
        String vesselCode = reqVO.getBkgVesselCode();
        String voy = reqVO.getBkgVoyage();
        String blno = reqVO.getBlNo();
        boolean check = StringUtils.isEmpty(blno) && (StringUtils.isEmpty(voy) || StringUtils.isEmpty(vesselCode));
        if (check) {
            throw ServiceExceptionUtil.exception(BlnoFreightErrorCodeConstants.FREIGHT_PARAM_VALID_FAILED);
        }
    }

    private void checkParamValid(MultiBlnoFreightReqVO reqVO) {
        String vesselCode = reqVO.getBkgVesselCode();
        String voy = reqVO.getBkgVoyage();
        String blno = reqVO.getBlNo();
        boolean check = StringUtils.isEmpty(blno) && (StringUtils.isEmpty(voy) || StringUtils.isEmpty(vesselCode));
        if (check) {
            throw ServiceExceptionUtil.exception(BlnoFreightErrorCodeConstants.FREIGHT_PARAM_VALID_FAILED);
        }
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Map<Long, String> calForecastFreight(Collection<Long> ids) {
        if (CollectionUtils.isEmpty(ids)) {
            return new HashMap<>(16);
        }
        Map<Long, String> errMsgMap = new HashMap<>(16);
        // 过滤无效id
        List<BlnoDO> blnoDoList = getBlnoCheckIds(ids, errMsgMap);
        if (CollectionUtils.isEmpty(blnoDoList)) {
            return errMsgMap;
        }
        ids = blnoDoList.stream().map(BlnoDO::getId).collect(Collectors.toSet());
        List<BlnoCntrDO> cntrDOList = blnoCntrService.getBlnoCntrByBlnoIds(ids);
        Set<String> cntrNoSet = cntrDOList.stream().map(BlnoCntrDO::getCntrNo).collect(Collectors.toSet());
        List<BlnoCntrDO> allCntrDoList = blnoCntrService.getBlnoCntrByCntrNos(cntrNoSet);
        // map<箱号, List<BlnoCntrDO>>，用于判断是否拼箱
        Map<String, List<BlnoCntrDO>> blnoCntrNoListMap =
            allCntrDoList.stream().collect(Collectors.groupingBy(BlnoCntrDO::getCntrNo));
        List<BlnoCargoDO> blnoCargoDOList = blnoCargoService.getBlnoCargoByBlnoIds(ids);
        List<BlnoVslvoyDO> blnoVslvoyDOList = blnoVslvoyService.getBlnoVslvoyBySiIdList(ids);
        List<BlnoScnDO> blnoScnDOList = blnoScnService.getBlnoScnByBlnoIdList(ids);
        List<BaseCodeDO> baseCodeDOList =
            baseCodeService.getBaseCodeListByType(BaseCodeCategoryEnum.SHIPPING_TERM.getValue());
        List<BaseCodeDO> baseCodeCargoList =
            baseCodeService.getBaseCodeListByType(BaseCodeCategoryEnum.CARGO_TYPE.getValue());
        // 船名航次map<船名航次, list>，用于判断是否拼箱
        List<BlnoDO> blnoLclList = blnoService.getBlnoWithLclList();
        Map<String, List<BlnoDO>> blnoListMap =
            blnoLclList.stream().collect(Collectors.groupingBy(item -> item.getBkgVesselCode() + item.getBkgVoyage()));
        // map<BlnoId, list>
        Map<Long, List<BlnoCntrDO>> cntrListMap =
            cntrDOList.stream().collect(Collectors.groupingBy(BlnoCntrDO::getBlnoId));
        // map<BlnoId, list>
        Map<Long, List<BlnoCargoDO>> cargoListMap =
            blnoCargoDOList.stream().collect(Collectors.groupingBy(BlnoCargoDO::getBlnoId));
        // map<BlnoId, list>
        Map<Long, List<BlnoVslvoyDO>> vslvoyListMap =
            blnoVslvoyDOList.stream().collect(Collectors.groupingBy(BlnoVslvoyDO::getBlnoId));
        // map<BlnoId, list>
        Map<Long, List<BlnoScnDO>> scnListMap =
            blnoScnDOList.stream().collect(Collectors.groupingBy(BlnoScnDO::getBlnoId));
        // SHIPPING_TERM map<code, descEn>
        Map<String, String> baseCodeNameMap =
            baseCodeDOList.stream().collect(Collectors.toMap(BaseCodeDO::getCode, BaseCodeDO::getDescEn));
        // CARGO_TYPE map<code, sortNo>
        Map<String, Integer> cargoSortMap =
            baseCodeCargoList.stream().collect(Collectors.toMap(BaseCodeDO::getCode, BaseCodeDO::getSortNo));

        // redis加锁
        Set<Long> lockIds = new HashSet<>();
        for (Long blnoId : ids) {
            if (redisUtils.checKeyExist(RedisKeyConstants.FREIGHT_KEY + blnoId)) {
                lockIds.add(blnoId);
                errMsgMap.put(blnoId, BlnoFreightConstants.REDIS_KEY_LOCKED);
            } else {
                redisUtils.setStringTimeOutByMinute(RedisKeyConstants.FREIGHT_KEY + blnoId, String.valueOf(blnoId),
                    redisTimeOut);
            }
        }
        if (CollectionUtils.isNotEmpty(lockIds)) {
            ids.removeAll(lockIds);
            blnoDoList =
                blnoDoList.stream().filter(blno -> !lockIds.contains(blno.getId())).collect(Collectors.toList());
        }
        if (CollectionUtils.isEmpty(ids)) {
            log.info(" all ids is lock");
            return errMsgMap;
        }

        List<BlnoFreightDO> blnoFreightDOList = new ArrayList<>();
        try {
            for (BlnoDO blnoDo : blnoDoList) {
                CalFrtRespVo calFrtRespVo = null;
                BlnoCargoDO blnoCargoDO = null;
                try {
                    // 通过营销接口获取海运费费率
                    blnoCargoDO = getBlnoCargo(cargoListMap, blnoDo.getId());
                    calFrtRespVo = getAndSaveFreightRate(blnoDo,
                        cntrListMap.getOrDefault(blnoDo.getId(), new ArrayList<>()), blnoCargoDO, vslvoyListMap,
                        scnListMap.getOrDefault(blnoDo.getId(), new ArrayList<>()), baseCodeNameMap);
                } catch (Exception e) {
                    log.error("get freight rate failed.", e);
                    errMsgMap.put(blnoDo.getId(), e.getMessage());
                }

                // 生成DO对象
                if (calFrtRespVo == null) {
                    continue;
                }
                List<BlnoFreightDO> blnoFreightDoListTmp = buildFreightList(calFrtRespVo, blnoDo, cntrListMap,
                    blnoCargoDO, blnoListMap, cargoSortMap, blnoCntrNoListMap);
                blnoFreightDOList.addAll(blnoFreightDoListTmp);
                errMsgMap.put(blnoDo.getId(), CommonConstants.RESPONSE_SUCCESS);
            }

            saveForecastFreightDb(blnoFreightDOList, ids);
        } finally {
            // 删除锁
            for (Long blnoId : ids) {
                redisUtils.deleteString(RedisKeyConstants.FREIGHT_KEY + blnoId);
            }
        }

        return errMsgMap;
    }

    private List<BlnoDO> getBlnoCheckIds(Collection<Long> ids, Map<Long, String> errMsgMap) {
        List<String> rejectStatuList = new ArrayList<>();
        rejectStatuList.add(SiStatusEnum.BookingDraft.getValue());
        rejectStatuList.add(SiStatusEnum.BookingProcessing.getValue());
        LambdaQueryWrapperX<BlnoDO> queryWrapperX = new LambdaQueryWrapperX<>();
        queryWrapperX.in(BlnoDO::getId, ids).eq(BlnoDO::getValid, SiValidEnum.VALID.getValue())
            .notIn(BlnoDO::getSiStatus, rejectStatuList).eq(BlnoDO::getPodBlFlag, true)
            .and(wrap -> wrap.isNull(BlnoDO::getDoStatus).or().eq(BlnoDO::getDoStatus, ""));
        List<BlnoDO> blnoDoList = blnoMapper.selectList(queryWrapperX);
        Set<Long> idDbs = blnoDoList.stream().map(BlnoDO::getId).collect(Collectors.toSet());
        ids.forEach(id -> {
            if (!idDbs.contains(id)) {
                errMsgMap.put(id, BlnoFreightConstants.INVALID_ID);
            }
        });

        return blnoDoList;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Map<Long, String> calOceanFreight(Collection<Long> ids) {
        if (CollectionUtils.isEmpty(ids)) {
            return new HashMap<>(16);
        }
        Map<Long, String> errMsgMap = new HashMap<>(16);
        // 过滤无效id
        List<BlnoDO> blnoDoList = getBlnoCheckIds(ids, errMsgMap);
        if (CollectionUtils.isEmpty(blnoDoList)) {
            return errMsgMap;
        }
        ids = blnoDoList.stream().map(BlnoDO::getId).collect(Collectors.toSet());
        List<BlnoCntrDO> cntrDOList = blnoCntrService.getBlnoCntrByBlnoIds(ids);
        Set<String> cntrNoSet = cntrDOList.stream().map(BlnoCntrDO::getCntrNo).collect(Collectors.toSet());
        List<BlnoCntrDO> allCntrDoList = blnoCntrService.getBlnoCntrByCntrNos(cntrNoSet);
        // map<箱号, List<BlnoCntrDO>>，用于判断是否拼箱
        Map<String, List<BlnoCntrDO>> blnoCntrNoListMap =
            allCntrDoList.stream().collect(Collectors.groupingBy(BlnoCntrDO::getCntrNo));
        List<BlnoCargoDO> blnoCargoDOList = blnoCargoService.getBlnoCargoByBlnoIds(ids);
        List<BlnoVslvoyDO> blnoVslvoyDOList = blnoVslvoyService.getBlnoVslvoyBySiIdList(ids);
        List<BlnoScnDO> blnoScnDOList = blnoScnService.getBlnoScnByBlnoIdList(ids);
        List<BaseCodeDO> baseCodeDOList =
            baseCodeService.getBaseCodeListByType(BaseCodeCategoryEnum.SHIPPING_TERM.getValue());
        List<BaseCodeDO> baseCodeCargoList =
            baseCodeService.getBaseCodeListByType(BaseCodeCategoryEnum.CARGO_TYPE.getValue());
        // 船名航次map<船名航次, list>，用于判断是否拼箱
        List<BlnoDO> blnoLclList = blnoService.getBlnoWithLclList();
        Map<String, List<BlnoDO>> blnoListMap =
            blnoLclList.stream().collect(Collectors.groupingBy(item -> item.getBkgVesselCode() + item.getBkgVoyage()));
        // map<BlnoId, list>
        Map<Long, List<BlnoCntrDO>> cntrListMap =
            cntrDOList.stream().collect(Collectors.groupingBy(BlnoCntrDO::getBlnoId));
        // map<BlnoId, list>
        Map<Long, List<BlnoCargoDO>> cargoListMap =
            blnoCargoDOList.stream().collect(Collectors.groupingBy(BlnoCargoDO::getBlnoId));
        // map<BlnoId, list>
        Map<Long, List<BlnoVslvoyDO>> vslvoyListMap =
            blnoVslvoyDOList.stream().collect(Collectors.groupingBy(BlnoVslvoyDO::getBlnoId));
        // map<BlnoId, list>
        Map<Long, List<BlnoScnDO>> scnListMap =
            blnoScnDOList.stream().collect(Collectors.groupingBy(BlnoScnDO::getBlnoId));
        // SHIPPING_TERM map<code, descEn>
        Map<String, String> baseCodeNameMap =
            baseCodeDOList.stream().collect(Collectors.toMap(BaseCodeDO::getCode, BaseCodeDO::getDescEn));
        // CARGO_TYPE map<code, sortNo>
        Map<String, Integer> cargoSortMap =
            baseCodeCargoList.stream().collect(Collectors.toMap(BaseCodeDO::getCode, BaseCodeDO::getSortNo));

        // redis加锁
        Set<Long> lockIds = new HashSet<>();
        for (Long blnoId : ids) {
            if (redisUtils.checKeyExist(RedisKeyConstants.FREIGHT_KEY + blnoId)) {
                lockIds.add(blnoId);
                errMsgMap.put(blnoId, BlnoFreightConstants.REDIS_KEY_LOCKED);
            } else {
                redisUtils.setStringTimeOutByMinute(RedisKeyConstants.FREIGHT_KEY + blnoId, String.valueOf(blnoId),
                    redisTimeOut);
            }
        }
        if (CollectionUtils.isNotEmpty(lockIds)) {
            ids.removeAll(lockIds);
            blnoDoList =
                blnoDoList.stream().filter(blno -> !lockIds.contains(blno.getId())).collect(Collectors.toList());
        }
        if (CollectionUtils.isEmpty(ids)) {
            log.info(" all ids is lock");
            return errMsgMap;
        }

        List<BlnoFreightDO> blnoFreightDOList = new ArrayList<>();
        List<BlnoFreightDetailDO> blnoFreightDetailDOList = new ArrayList<>();
        Map<Long, List<BlnoFreightDetailDO>> fcnFreightDetailListMap = new HashMap<>(CommonConstants.DEFAULT_MAP_COUNT);
        Set<Long> fcnBlnoIds = new HashSet<>();
        try {
            for (BlnoDO blnoDo : blnoDoList) {
                CalFrtRespVo calFrtRespVo = null;
                BlnoCargoDO blnoCargoDO = null;
                try {
                    // 通过营销接口获取海运费费率
                    blnoCargoDO = getBlnoCargo(cargoListMap, blnoDo.getId());
                    calFrtRespVo = getAndSaveFreightRate(blnoDo,
                        cntrListMap.getOrDefault(blnoDo.getId(), new ArrayList<>()), blnoCargoDO, vslvoyListMap,
                        scnListMap.getOrDefault(blnoDo.getId(), new ArrayList<>()), baseCodeNameMap);
                } catch (Exception e) {
                    log.error("get freight rate failed.", e);
                    errMsgMap.put(blnoDo.getId(), e.getMessage());
                }

                // 生成DO对象
                if (calFrtRespVo == null) {
                    continue;
                }

                // 生成DO对象
                List<BlnoFreightDO> blnoFreightDoListTmp = buildFreightList(calFrtRespVo, blnoDo, cntrListMap,
                    blnoCargoDO, blnoListMap, cargoSortMap, blnoCntrNoListMap);
                List<BlnoFreightDetailDO> blnoFreightDetailDoListTmp = buildFreightDetailList(calFrtRespVo, blnoDo,
                    cntrListMap, blnoCargoDO, blnoListMap, cargoSortMap, blnoCntrNoListMap);

                blnoFreightDetailDOList.addAll(blnoFreightDetailDoListTmp);
                blnoFreightDOList.addAll(blnoFreightDoListTmp);
                if (costChargeUtil.checkLockVoyage(vslvoyListMap.getOrDefault(blnoDo.getId(), new ArrayList<>()))) {
                    fcnFreightDetailListMap.put(blnoDo.getId(), blnoFreightDetailDoListTmp);
                    fcnBlnoIds.add(blnoDo.getId());
                }
                errMsgMap.put(blnoDo.getId(), CommonConstants.RESPONSE_SUCCESS);
            }

            saveFcnFreightDb(fcnFreightDetailListMap, fcnBlnoIds);
            saveOceanFreightDb(blnoFreightDOList, blnoFreightDetailDOList, ids);
        } catch (Exception e) {
            log.error("unknown exception on calOceanFreight", e);
            Assertion.assertTrue(false).raise0(BlnoFreightErrorCodeConstants.FREIGHT_UNKNOW_EXCEPTION.getCode(),
                e.toString());
        } finally {
            // 删除锁
            for (Long blnoId : ids) {
                redisUtils.deleteString(RedisKeyConstants.FREIGHT_KEY + blnoId);
            }
        }

        return errMsgMap;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Map<Long, String> toAccountFreight(Collection<Long> ids) {
        Map<Long, String> errMsgMap = new HashMap<>(16);
        // 过滤无效id
        List<BlnoDO> blnoDoList = getBlnoCheckIds(ids, errMsgMap);
        if (CollectionUtils.isEmpty(blnoDoList)) {
            return errMsgMap;
        }
        ids = blnoDoList.stream().map(BlnoDO::getId).collect(Collectors.toSet());
        // redis加锁
        Set<Long> lockIds = new HashSet<>();
        for (Long blnoId : ids) {
            if (redisUtils.checKeyExist(RedisKeyConstants.FREIGHT_KEY + blnoId)) {
                lockIds.add(blnoId);
                errMsgMap.put(blnoId, BlnoFreightConstants.REDIS_KEY_LOCKED);
            } else {
                redisUtils.setStringTimeOutByMinute(RedisKeyConstants.FREIGHT_KEY + blnoId, String.valueOf(blnoId),
                    redisTimeOut);
                errMsgMap.put(blnoId, CommonConstants.RESPONSE_SUCCESS);
            }
        }
        if (CollectionUtils.isNotEmpty(lockIds)) {
            ids.removeAll(lockIds);
            blnoDoList =
                blnoDoList.stream().filter(blno -> !lockIds.contains(blno.getId())).collect(Collectors.toList());
        }
        if (CollectionUtils.isEmpty(ids)) {
            log.info(" all ids is lock");
            return errMsgMap;
        }

        LambdaQueryWrapperX<BlnoFreightDO> queryWrapperX = new LambdaQueryWrapperX<>();
        queryWrapperX.eq(BlnoFreightDO::getToAccount, false).in(BlnoFreightDO::getBlnoId, ids);
        List<BlnoFreightDO> blnoFreightDOList = blnoFreightMapper.selectList(queryWrapperX);
        if (CollectionUtils.isEmpty(blnoFreightDOList)) {
            log.info("no freight need to send to account");
            return errMsgMap;
        }
        try {
            List<BlnoVslvoyDO> blnoVslvoyDOList = blnoVslvoyService.getBlnoVslvoyBySiIdList(ids);
            DocBlnoBaseReqDTO docBlnoBaseReqDTO =
                buildDocBlnoBaseReqDTO(blnoFreightDOList, blnoDoList, blnoVslvoyDOList);
            boolean success = accountTransfer.createBlnoFreight(docBlnoBaseReqDTO);
            if (!success) {
                throw ServiceExceptionUtil.exception(BlnoFreightErrorCodeConstants.BLNO_FREIGHT_SEND_ACCOUNT_FAILED);
            }
            blnoFreightDOList.forEach(freight -> freight.setToAccount(true));
            blnoFreightMapper.updateBatch(blnoFreightDOList);
        } finally {
            for (Long blnoId : ids) {
                redisUtils.deleteString(RedisKeyConstants.FREIGHT_KEY + blnoId);
            }
        }
        return errMsgMap;
    }

    private DocBlnoBaseReqDTO buildDocBlnoBaseReqDTO(List<BlnoFreightDO> blnoFreightDOList, List<BlnoDO> blnoDOList,
        List<BlnoVslvoyDO> blnoVslvoyDOList) {
        Map<Long, List<BlnoVslvoyDO>> vslvoyListMap =
            blnoVslvoyDOList.stream().collect(Collectors.groupingBy(BlnoVslvoyDO::getBlnoId));
        Map<Long, List<BlnoFreightDO>> freightListMap =
            blnoFreightDOList.stream().collect(Collectors.groupingBy(BlnoFreightDO::getBlnoId));
        DocBlnoBaseReqDTO docBlnoBaseReq = new DocBlnoBaseReqDTO();
        List<DocBlnoCreateReqDTO> docBlnoList = new ArrayList<>();
        blnoDOList.forEach(blnoDO -> {
            DocBlnoCreateReqDTO blnoReq = BlnoFreightConvert.INSTANCE.convertAccountReq(blnoDO);
            blnoReq.setSender(String.valueOf(SecurityFrameworkUtils.getLoginUserId()));
            LocalDateTime now = LocalDateTime.now();
            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
            String formattedString = now.format(formatter);
            blnoReq.setSendTime(formattedString);
            List<DocBlnoVslvoyReqDTO> docBlnoVslvoyList =
                buildVslvoyReq(vslvoyListMap.getOrDefault(blnoDO.getId(), new ArrayList<>()));
            List<DocBlnoFreightInfoReqDTO> freightInfoList =
                buildFreightReq(freightListMap.getOrDefault(blnoDO.getId(), new ArrayList<>()), blnoDO.getPolCode());
            blnoReq.setDocBlnoVslvoyList(docBlnoVslvoyList);
            blnoReq.setFreightInfoList(freightInfoList);
            blnoReq.setPayType(PayTypeEnum.INCOME.getValue());
            docBlnoList.add(blnoReq);
        });
        docBlnoBaseReq.setDocBlnoList(docBlnoList);
        return docBlnoBaseReq;
    }

    private List<DocBlnoFreightInfoReqDTO> buildFreightReq(List<BlnoFreightDO> blnoFreightDOList, String localPort) {
        List<DocBlnoFreightInfoReqDTO> vslvoyReqList = new ArrayList<>();
        blnoFreightDOList.forEach(freight -> {
            DocBlnoFreightInfoReqDTO freightReq = BlnoFreightConvert.INSTANCE.convertFreightReq(freight);
            freightReq.setLocalPortCode(localPort);
            vslvoyReqList.add(freightReq);
        });
        return vslvoyReqList;
    }

    private void saveFcnFreightDb(Map<Long, List<BlnoFreightDetailDO>> detailDoListMap, Set<Long> ids) {
        if (CollectionUtils.isEmpty(ids)) {
            return;
        }

        List<BlnoFreightDetailDO> detaiDblList = blnoFreightDetailService.getBlnoFreightDetailByBlnoIdList(ids);
        List<ConfirmNoticeFcnDO> originalAllList = BlnoFreightDetailConvert.INSTANCE.convertFcnList(detaiDblList);
        Map<Long, List<ConfirmNoticeFcnDO>> originalListMap =
            originalAllList.stream().collect(Collectors.groupingBy(ConfirmNoticeFcnDO::getBlnoId));
        Map<Long, List<ConfirmNoticeFcnDO>> calculateListMap = new HashMap<>(CommonConstants.DEFAULT_MAP_COUNT);
        detailDoListMap.forEach((key, value) -> {
            List<ConfirmNoticeFcnDO> tmpFcnList = BlnoFreightDetailConvert.INSTANCE.convertFcnList(value);
            calculateListMap.put(key, tmpFcnList);
        });

        Set<Long> noChangeIds = new HashSet<>();
        ids.forEach(id -> {
            List<ConfirmNoticeFcnDO> originalList = originalListMap.getOrDefault(id, new ArrayList<>());
            List<ConfirmNoticeFcnDO> reculateList = calculateListMap.getOrDefault(id, new ArrayList<>());
            if (checkNoChange(originalList, reculateList)) {
                noChangeIds.add(id);
                originalListMap.remove(id);
                calculateListMap.remove(id);
            }
        });
        ids.removeAll(noChangeIds);
        if (CollectionUtils.isEmpty(ids)) {
            return;
        }

        // 保存FNC主表
        List<BlnoDO> blnoDOList = blnoService.getBlnoList(ids);
        List<ConfirmNoticeDO> confirmNoticeDoList = ConfirmNoticeConvert.INSTANCE.convertFromBlno(blnoDOList);
        confirmNoticeMapper.insertBatch(confirmNoticeDoList);

        // 合并FCN待保存子表
        List<ConfirmNoticeFcnDO> originalList =
            originalListMap.values().stream().flatMap(List::stream).collect(Collectors.toList());
        List<ConfirmNoticeFcnDO> reculateList =
            calculateListMap.values().stream().flatMap(List::stream).collect(Collectors.toList());

        // FCN关联主表设置noticeId
        Map<Long, Long> confirmNoticeMap = confirmNoticeDoList.stream().collect(
            Collectors.toMap(ConfirmNoticeDO::getBlnoId, ConfirmNoticeDO::getId, (oldValue, newValue) -> newValue));
        originalList.forEach(notice -> {
            Long blnoId = notice.getBlnoId();
            Long noticeId = confirmNoticeMap.get(blnoId);
            notice.setNoticeId(noticeId);
            notice.setFrtType(FrtCalTypesEnum.ORIGINAL.getValue());
        });
        reculateList.forEach(notice -> {
            Long blnoId = notice.getBlnoId();
            Long noticeId = confirmNoticeMap.get(blnoId);
            notice.setNoticeId(noticeId);
            notice.setFrtType(FrtCalTypesEnum.RECALCULATE.getValue());
        });

        // 保存FCN数据表
        if (!CollectionUtils.isEmpty(originalList)) {
            confirmNoticeFcnMapper.insertBatch(originalList);
        }
        if (!CollectionUtils.isEmpty(reculateList)) {
            confirmNoticeFcnMapper.insertBatch(reculateList);
        }
    }

    private boolean checkNoChange(List<ConfirmNoticeFcnDO> originalList, List<ConfirmNoticeFcnDO> reculateList) {
        originalList.forEach(notice -> notice.setChangeStatus(ConfirmChangeStatusEnum.CHANGE.getValue()));
        reculateList.forEach(notice -> notice.setChangeStatus(ConfirmChangeStatusEnum.CHANGE.getValue()));
        for (ConfirmNoticeFcnDO original : originalList) {
            for (ConfirmNoticeFcnDO reculate : reculateList) {
                if (checkEqual(original, reculate)) {
                    reculate.setChangeStatus(ConfirmChangeStatusEnum.NOT_CHANGE.getValue());
                    original.setChangeStatus(ConfirmChangeStatusEnum.NOT_CHANGE.getValue());
                    break;
                }
            }
        }

        boolean existChange = reculateList.stream()
            .anyMatch(rec -> ConfirmChangeStatusEnum.CHANGE.getValue().equals(rec.getChangeStatus()));
        return !existChange && originalList.size() == reculateList.size();
    }

    private boolean checkEqual(ConfirmNoticeFcnDO obj1, ConfirmNoticeFcnDO obj2) {
        return Objects.equals(obj1.getFreightId(), obj2.getFreightId())
            && Objects.equals(obj1.getBlnoId(), obj2.getBlnoId())
            && Objects.equals(obj1.getChargeCode(), obj2.getChargeCode())
            && Objects.equals(obj1.getPer(), obj2.getPer()) && Objects.equals(obj1.getCargoType(), obj2.getCargoType())
            && Objects.equals(obj1.getContainerNo(), obj2.getContainerNo())
            && Objects.equals(obj1.getCntrType(), obj2.getCntrType())
            && Objects.equals(obj1.getCntrSize(), obj2.getCntrSize())
            && Objects.equals(obj1.getQuantity(), obj2.getQuantity()) && Objects.equals(obj1.getRate(), obj2.getRate())
            && Objects.equals(obj1.getCurrency(), obj2.getCurrency())
            && Objects.equals(obj1.getPayment(), obj2.getPayment()) && Objects.equals(obj1.getPayer(), obj2.getPayer())
            && Objects.equals(obj1.getChangeStatus(), obj2.getChangeStatus());
    }

    private void saveOceanFreightDb(List<BlnoFreightDO> blnoFreightDOList,
        List<BlnoFreightDetailDO> blnoFreightDetailDOList, Collection<Long> ids) {
        if (!CollectionUtils.isEmpty(blnoFreightDetailDOList)) {
            freightDetailService.deleteByBlnoIdList(ids);
            freightDetailService.insertBatchFreight(blnoFreightDetailDOList);
        }

        if (!CollectionUtils.isEmpty(blnoFreightDOList)) {
            // 删除未发送业财的DO对象
            LambdaQueryWrapperX<BlnoFreightDO> deleteWrapperX = new LambdaQueryWrapperX<>();
            deleteWrapperX.eq(BlnoFreightDO::getToAccount, false).in(BlnoFreightDO::getBlnoId, ids);
            blnoFreightMapper.delete(deleteWrapperX);

            // 获取已发送业财的DO对象，过滤已经红冲的数据
            Map<String, BlnoFreightDO> freightAccountMap = getFreightAccountFilterMap(ids);

            // 待保存数据库中的数据表
            List<BlnoFreightDO> freightDbList = new ArrayList<>();
            Set<String> existKey = new HashSet<>();
            // 生成db对象，时间复杂度O(n+m), 空间复杂度T(n+m)
            for (BlnoFreightDO freight : blnoFreightDOList) {
                String key = buildKey(freight);
                // 存在同一条费用记录
                if (freightAccountMap.containsKey(key)) {
                    existKey.add(key);
                    BlnoFreightDO freightAccount = freightAccountMap.get(key);
                    // 费率不同，生成红冲记录
                    if (freight.getAmount().compareTo(freightAccount.getAmount()) != 0
                        || !freight.getPer().equals(freightAccount.getPer())) {
                        BlnoFreightDO freightTmp = buildDashedBackFreight(freightAccount);
                        freightDbList.add(freightTmp);
                        freightDbList.add(freight);
                        freightAccount.setRedStatus(RedStatusEnum.RED.getValue());
                        freightDbList.add(freightAccount);
                    }
                } else {
                    // 发送财务数据表不存在该记录，保存至数据表
                    freightDbList.add(freight);
                }
            }

            // 处理已经发送给财务，但是此次计算没有的数据，生成红冲记录
            for (Map.Entry<String, BlnoFreightDO> entry : freightAccountMap.entrySet()) {
                String key = entry.getKey();
                if (existKey.contains(key)) {
                    continue;
                }
                BlnoFreightDO freightAccount = entry.getValue();
                freightAccount.setRedStatus(RedStatusEnum.RED.getValue());
                BlnoFreightDO freightTmp = buildDashedBackFreight(freightAccount);
                freightDbList.add(freightTmp);
                freightDbList.add(freightAccount);
            }

            blnoFreightMapper.saveOrUpdateBatch(freightDbList);
        }
    }

    private BlnoFreightDO buildDashedBackFreight(BlnoFreightDO freightAccount) {
        BlnoFreightDO freightTmp = BlnoFreightConvert.INSTANCE.convertFrightDO(freightAccount);
        freightTmp.setToAccount(false);
        freightTmp.setRate(freightAccount.getRate().negate());
        freightTmp.setAmount(freightAccount.getAmount().negate());
        freightTmp.setOriginalId(freightAccount.getId());
        freightTmp.setId(null);
        freightTmp.setRedStatus(RedStatusEnum.RED.getValue());
        return freightTmp;
    }

    private Map<String, BlnoFreightDO> getFreightAccountFilterMap(Collection<Long> ids) {
        List<BlnoFreightDO> freightAccountList = getBlnoFreightByBlnoIdList(ids);
        Set<Long> originalIds = freightAccountList.stream().map(BlnoFreightDO::getOriginalId).filter(Objects::nonNull)
            .collect(Collectors.toSet());
        return freightAccountList.stream()
            .filter(item -> !originalIds.contains(item.getId()) && item.getOriginalId() == null)
            .collect(Collectors.toMap(this::buildKey, freight -> freight, (existingValue, newValue) -> existingValue));
    }

    private String buildKey(BlnoFreightDO freight) {
        StringBuilder sb = new StringBuilder();
        if (freight.getCurrency() != null) {
            sb.append(freight.getCurrency());
        }

        if (freight.getBlnoId() != null) {
            sb.append(freight.getBlnoId());
        }

        if (freight.getChargeCode() != null) {
            sb.append(freight.getChargeCode());
        }

        if (freight.getCargoType() != null) {
            sb.append(freight.getCargoType());
        }

        if (freight.getCntrType() != null) {
            sb.append(freight.getCntrType());
        }

        if (freight.getCntrSize() != null) {
            sb.append(freight.getCntrSize());
        }

        if (freight.getQuantity() != null) {
            sb.append(freight.getQuantity());
        }

        if (freight.getPayment() != null) {
            sb.append(freight.getPayment());
        }

        return sb.toString();
    }

    private void saveForecastFreightDb(List<BlnoFreightDO> blnoFreightDOList, Collection<Long> ids) {
        if (!CollectionUtils.isEmpty(blnoFreightDOList)) {
            // 删除所有数据
            freightForecastService.deleteBlnoFreightForecast(ids);
            List<BlnoFreightForecastDO> forecastDOList =
                BlnoFreightConvert.INSTANCE.convertFreightForecastList(blnoFreightDOList);
            freightForecastService.insertBatchBlnoFreightForecast(forecastDOList);
        }
    }

    /**
     * 检查航次是否确认
     *
     * @param blnoDO 提单信息
     * @return boolean
     */
    private boolean checkVoyConfirm(BlnoDO blnoDO) {
        return blnoDO.getBlLockFlag();
    }

    /**
     * 创建fcn序列号： 1. 创建FCN数据表 2. 航次锁定且费用计算发给业财之后，且费用有变化，创建FCN，保存到FCN数据表 3. 发送MQ消息待定
     *
     * @param blnoDO 提单信息
     * @return java.lang.String
     */
    private String createFcnSeqNo(BlnoDO blnoDO) {
        // 创建fcn序列号
        // TODO
        return null;
    }

    /**
     * 判断DO状态：DO不为空则已经DO，不进行费用计算，待取消DO后再计算
     *
     * @param blnoDO 提单信息
     * @return boolean
     */
    private boolean checkDo(BlnoDO blnoDO) {
        return StringUtils.isEmpty(blnoDO.getDoStatus());
    }

    private CalFrtRespVo getAndSaveFreightRate(BlnoDO blnoDO, List<BlnoCntrDO> blnoCntrDOList, BlnoCargoDO blnoCargoDO,
        Map<Long, List<BlnoVslvoyDO>> vslvoyListMap, List<BlnoScnDO> scnList, Map<String, String> baseCodeNameMap) {
        CalFrtReqVo calFrtReqVo = null;
        CalFrtRespVo calFrtRespVo = null;
        try {
            // 构造海运费率计算请求参数
            calFrtReqVo =
                buildCalFrtReqVo(blnoDO, blnoCntrDOList, blnoCargoDO, vslvoyListMap, scnList, baseCodeNameMap);

            // 获取海运费费率
            calFrtRespVo = rateTransfer.calForecastFreight(calFrtReqVo);
            if (calFrtRespVo == null) {
                log.error("getAndSaveFreightRate return null, blnoId: {}", blnoDO.getId());
                throw new Exception(GET_RATE_NULL);
            }
            if (calFrtRespVo.getResultCode() == null || calFrtRespVo.getResultCode() != 0) {
                log.error("getAndSaveFreightRate return code not 0, blnoId: {}", blnoDO.getId());
                throw new Exception(
                    GET_RATE_CODE_ERROR + calFrtRespVo.getResultCode() + MSG + calFrtRespVo.getResultMsg());
            }
        } catch (Exception e) {
            // 异常不抛出，仅保存日志表
            log.error("caught an exception on getFreightRate, blnoId: {}", blnoDO.getId(), e);
            saveCalLog(blnoDO, calFrtReqVo, calFrtRespVo, blnoCntrDOList,
                CalculateTypeEnum.CALCULATE_FORECAST_FREIGHT.getValue(), CalculateStatusEnum.EXCEPTION.getValue(),
                e.toString());
            throw ExceptionUtil.exception(BlnoFreightErrorCodeConstants.GET_BLNO_RATE_FAILED, e.toString());
        }

        // 计费操作保存日志
        saveCalLog(blnoDO, calFrtReqVo, calFrtRespVo, blnoCntrDOList,
            CalculateTypeEnum.CALCULATE_FORECAST_FREIGHT.getValue(), CalculateStatusEnum.SUCCESS.getValue(), null);

        return calFrtRespVo;
    }

    private BigDecimal calWgtFreight(String payment, BlnoDO blnoDO, BlnoCargoDO blnoCargoDO) {
        BigDecimal mwgt = blnoCargoDO.getMeasurement();
        BigDecimal gwgt = blnoCargoDO.getGrossWeight().divide(new BigDecimal(1000));
        BigDecimal wgt = mwgt.compareTo(gwgt) > 0 ? mwgt : gwgt;
        String delCode = blnoDO.getDelCode();
        if (PaymentEnum.A.getValue().equalsIgnoreCase(payment) || PaymentEnum.C.getValue().equalsIgnoreCase(payment)
            || PaymentEnum.P.getValue().equalsIgnoreCase(payment)) {
            wgt = dealWithPayment(delCode, wgt);
        }

        if (wgt.compareTo(new BigDecimal(1)) < 0) {
            wgt = new BigDecimal(1);
        }
        return wgt;
    }

    private BigDecimal dealWithPayment(String delCode, BigDecimal wgt) {

        switch (delCode) {
            case "CNTAO":
                // 不满2个体积吨，按2算
                if (wgt.compareTo(MIN_WEIGHT_TWO) < 0) {
                    return MIN_WEIGHT_TWO;
                }
                break;
            case "CNSHA":
            case "CNNGB":
                // 不满3个体积吨，按3算
                if (wgt.compareTo(MIN_WEIGHT_THREE) < 0) {
                    return MIN_WEIGHT_THREE;
                }
                break;
            case "CNTXG":
            case "CNDLC":
            default:
                // 不满1个体积吨，按1算
                if (wgt.compareTo(MIN_WEIGHT_ONE) < 0) {
                    return MIN_WEIGHT_ONE;
                }
                break;
        }
        return wgt;
    }

    /**
     * 判断是否拼箱 拼箱且不是最高优先级返回true, 其它返回false； 货类值越大，优先级越低； 货类相同判断提单号，提单号越小，优先级越高
     *
     * @param cntrNo 箱号
     * @param blnoDo 提单信息
     * @param blnoListMap 提单Map
     * @param blnoCargoDO blnoCargoDO
     * @return 是否拼箱
     */
    private boolean checkLcl(String cntrNo, BlnoDO blnoDo, Map<String, List<BlnoDO>> blnoListMap,
        BlnoCargoDO blnoCargoDO, Map<String, Integer> cargoSortMap, Map<String, List<BlnoCntrDO>> blnoCntrNoListMap) {
        List<BlnoCntrDO> blnoCntrDOList = blnoCntrNoListMap.getOrDefault(cntrNo, new ArrayList<>());
        if (blnoCntrDOList.size() <= 1) {
            return false;
        }
        // 获取同箱型的所有提单id
        Set<Long> cntrSet = blnoCntrDOList.stream().map(BlnoCntrDO::getBlnoId).collect(Collectors.toSet());
        int curCargoSort = getBlnoCargoSort(blnoCargoDO, blnoDo.getId(), cargoSortMap);
        String key = blnoDo.getBkgVesselCode() + blnoDo.getBkgVoyage();
        List<BlnoDO> blnoDOList = blnoListMap.getOrDefault(key, new ArrayList<>());
        if (blnoDOList.size() == 1) {
            return false;
        }
        for (BlnoDO blnoTmp : blnoDOList) {
            if (!cntrSet.contains(blnoTmp.getId())) {
                continue;
            }
            int tmpCargoSort = getBlnoCargoSort(blnoCargoDO, blnoTmp.getId(), cargoSortMap);
            if (tmpCargoSort < curCargoSort) {
                return true;
            } else if (tmpCargoSort == curCargoSort && blnoTmp.getBlNo().compareTo(blnoDo.getBlNo()) < 0) {
                return true;
            }
        }
        return false;
    }

    private int getBlnoCargoSort(BlnoCargoDO blnoCargoDO, Long blnoId, Map<String, Integer> cargoSortMap) {
        String cargoType = blnoCargoDO.getCargoType();
        if (cargoSortMap.containsKey(cargoType)) {
            return cargoSortMap.get(cargoType);
        } else {
            log.error("cargo sort not exist, cargoType:{}, blnoId:{}", cargoType, blnoId);
            throw ServiceExceptionUtil.exception(BlnoFreightErrorCodeConstants.BLNO_CARGO_SORT_NOT_EXISTS);
        }
    }

    private BlnoCargoDO getBlnoCargo(Map<Long, List<BlnoCargoDO>> cargoListMap, Long blnoId) {
        List<BlnoCargoDO> curCargoList = cargoListMap.getOrDefault(blnoId, new ArrayList<>());
        if (CollectionUtils.isEmpty(curCargoList)) {
            log.error("no find cargo, blnoId: {}", blnoId);
            throw ServiceExceptionUtil.exception(BlnoFreightErrorCodeConstants.BLNO_CARGO_NOT_EXISTS);
        }
        return curCargoList.get(0);
    }

    private List<BlnoFreightDO> buildFreightList(CalFrtRespVo calFrtRespVo, BlnoDO blnoDO,
        Map<Long, List<BlnoCntrDO>> cntrListMap, BlnoCargoDO blnoCargoDO, Map<String, List<BlnoDO>> blnoListMap,
        Map<String, Integer> cargoSortMap, Map<String, List<BlnoCntrDO>> blnoCntrNoListMap) {
        List<BlnoFreightDO> freightList = new ArrayList<>();
        List<CalFrtRateUnitRespVo> rateUnitList = calFrtRespVo.getRateUnits();
        if (CollectionUtils.isEmpty(rateUnitList)) {
            return freightList;
        }

        for (CalFrtRateUnitRespVo rateUnit : rateUnitList) {
            List<FeeRateVo> feeDetails = rateUnit.getFeeDetails();
            if (CollectionUtils.isEmpty(feeDetails)) {
                continue;
            }
            for (FeeRateVo feeRate : feeDetails) {
                BlnoFreightDO freight = new BlnoFreightDO();
                freight.setBlnoId(blnoDO.getId());
                freight.setCargoType(blnoCargoDO.getCargoType());
                freight.setCargoType(blnoCargoDO.getCargoType());
                freight.setChargeCode(feeRate.getFeeCode());
                freight.setPer(transitCalPer(rateUnit.getCntrSizeType()));
                if (rateUnit.getCntrSize() != null) {
                    freight.setCntrSize(String.valueOf(rateUnit.getCntrSize()));
                }
                freight.setCntrType(rateUnit.getCntrType());
                freight.setRate(feeRate.getRate().setScale(4, RoundingMode.HALF_UP));
                freight.setCurrency(feeRate.getCur());
                freight.setPayer(getPayer(feeRate.getPayerCode(), feeRate.getPayTerm(), blnoDO.getBkpCode()));
                freight.setPayment(feeRate.getPayTerm());
                freight.setToAccount(false);
                freight.setRecordType(RecordTyepEnum.AUTO.getValue());
                freight.setAgreementNo(feeRate.getAgmtType());

                // 获取quantity数量
                if (CalculatePerEnum.BILL.getValue().equals(freight.getPer())) {
                    freight.setQuantity(new BigDecimal(1).setScale(3, RoundingMode.HALF_UP));
                } else if (CalculatePerEnum.CBM.getValue().equals(freight.getPer())) {
                    BigDecimal wgt = calWgtFreight(feeRate.getPayTerm(), blnoDO, blnoCargoDO);
                    freight.setQuantity(wgt.setScale(3, RoundingMode.HALF_UP));
                } else {
                    int count = calQuantity(cntrListMap, rateUnit, blnoCntrNoListMap, blnoDO, blnoListMap, blnoCargoDO,
                        cargoSortMap);
                    if (count == 0) {
                        continue;
                    }
                    freight.setQuantity(new BigDecimal(count).setScale(3, RoundingMode.HALF_UP));
                }

                // 计算account
                BigDecimal amount = CommonUtil.decimalMulHalfUp(feeRate.getRate(), freight.getQuantity(),
                    freight.getCurrency(), freight.getPer());
                freight.setAmount(amount);

                freightList.add(freight);
            }
        }

        return freightList;
    }

    private List<BlnoFreightDetailDO> buildFreightDetailList(CalFrtRespVo calFrtRespVo, BlnoDO blnoDO,
        Map<Long, List<BlnoCntrDO>> cntrListMap, BlnoCargoDO blnoCargoDO, Map<String, List<BlnoDO>> blnoListMap,
        Map<String, Integer> cargoSortMap, Map<String, List<BlnoCntrDO>> blnoCntrNoListMap) {
        List<BlnoFreightDetailDO> freightList = new ArrayList<>();
        List<CalFrtRateUnitRespVo> rateUnitList = calFrtRespVo.getRateUnits();
        if (CollectionUtils.isEmpty(rateUnitList)) {
            return freightList;
        }

        for (CalFrtRateUnitRespVo rateUnit : rateUnitList) {
            List<FeeRateVo> feeDetails = rateUnit.getFeeDetails();
            if (CollectionUtils.isEmpty(feeDetails)) {
                continue;
            }
            for (FeeRateVo feeRate : feeDetails) {
                BlnoFreightDetailDO freight = new BlnoFreightDetailDO();
                freight.setBlnoId(blnoDO.getId());
                freight.setCargoType(blnoCargoDO.getCargoType());
                freight.setCargoType(blnoCargoDO.getCargoType());
                freight.setChargeCode(feeRate.getFeeCode());
                freight.setPer(transitCalPer(rateUnit.getCntrSizeType()));
                if (rateUnit.getCntrSize() != null) {
                    freight.setCntrSize(String.valueOf(rateUnit.getCntrSize()));
                }
                freight.setCntrType(rateUnit.getCntrType());
                freight.setRate(feeRate.getRate().setScale(4, RoundingMode.HALF_UP));
                freight.setCurrency(feeRate.getCur());
                freight.setPayer(getPayer(feeRate.getPayerCode(), feeRate.getPayTerm(), blnoDO.getBkpCode()));
                freight.setPayment(feeRate.getPayTerm());
                freight.setRecordType(RecordTyepEnum.AUTO.getValue());
                freight.setAgreementNo(feeRate.getAgmtType());

                // 获取quantity数量
                if (CalculatePerEnum.BILL.getValue().equals(freight.getPer())
                    || CalculatePerEnum.CBM.getValue().equals(freight.getPer())) {
                    if (CalculatePerEnum.BILL.getValue().equals(freight.getPer())) {
                        freight.setQuantity(new BigDecimal(1).setScale(3, RoundingMode.HALF_UP));
                    } else {
                        BigDecimal wgt = calWgtFreight(feeRate.getPayTerm(), blnoDO, blnoCargoDO);
                        freight.setQuantity(wgt.setScale(3, RoundingMode.HALF_UP));
                    }

                    // 计算account
                    BigDecimal amount = CommonUtil.decimalMulHalfUp(feeRate.getRate(), freight.getQuantity(),
                        freight.getCurrency(), freight.getPer());
                    freight.setAmount(amount);
                    freightList.add(freight);
                } else {
                    // 按箱
                    List<BlnoCntrDO> cntrList = cntrListMap.getOrDefault(blnoDO.getId(), new ArrayList<>());
                    if (CollectionUtils.isEmpty(cntrList)) {
                        continue;
                    }

                    for (BlnoCntrDO cntr : cntrList) {
                        if (cntr.getCntrSize().equals(String.valueOf(rateUnit.getCntrSize()))
                            && cntr.getCntrType().equals(rateUnit.getCntrType())
                            && cntr.getCntrOwner().equals(rateUnit.getCntrOwnerType())) {
                            BlnoFreightDetailDO tempFreight = BlnoFreightConvert.INSTANCE.convert(freight);
                            tempFreight.setContainerNo(cntr.getCntrNo());
                            if (checkLcl(cntr.getCntrNo(), blnoDO, blnoListMap, blnoCargoDO, cargoSortMap,
                                blnoCntrNoListMap)) {
                                continue;
                            } else {
                                tempFreight.setQuantity(new BigDecimal(1).setScale(3, RoundingMode.HALF_UP));
                            }
                            BigDecimal amount = CommonUtil.decimalMulHalfUp(feeRate.getRate(),
                                tempFreight.getQuantity(), freight.getCurrency(), freight.getPer());
                            tempFreight.setAmount(amount);
                            freightList.add(tempFreight);
                        }
                    }
                }
            }
        }

        return freightList;
    }

    private String getPayer(String ratePay, String payment, String bkpCode) {
        // 协议返回了结算方
        if (PaymentEnum.A.getValue().equals(payment) || PaymentEnum.R.getValue().equals(payment)
            || PaymentEnum.M.getValue().equals(payment)) {
            return ratePay;
        }

        // 订舱客户指定了结算方
        // 待实现

        // P费用默认订舱人BKP
        if (PaymentEnum.P.getValue().equals(payment)) {
            return bkpCode;
        }

        return StringUtils.EMPTY;
    }

    private int calQuantity(Map<Long, List<BlnoCntrDO>> cntrListMap, CalFrtRateUnitRespVo rateUnit,
        Map<String, List<BlnoCntrDO>> blnoCntrNoListMap, BlnoDO blnoDo, Map<String, List<BlnoDO>> blnoListMap,
        BlnoCargoDO blnoCargoDO, Map<String, Integer> cargoSortMap) {
        List<BlnoCntrDO> cntrList = cntrListMap.getOrDefault(blnoDo.getId(), new ArrayList<>());
        if (CollectionUtils.isEmpty(cntrList)) {
            return 0;
        }
        int count = 0;
        for (BlnoCntrDO cntr : cntrList) {
            if (cntr.getCntrSize().equals(String.valueOf(rateUnit.getCntrSize()))
                && cntr.getCntrType().equals(rateUnit.getCntrType())
                && cntr.getCntrOwner().equals(rateUnit.getCntrOwnerType())) {
                if (!checkLcl(cntr.getCntrNo(), blnoDo, blnoListMap, blnoCargoDO, cargoSortMap, blnoCntrNoListMap)) {
                    count++;
                }
            }
        }

        return count;
    }

    private String transitCalPer(String ratePayment) {
        if (RATE_PAYMENT_BILL.equalsIgnoreCase(ratePayment)) {
            return CalculatePerEnum.BILL.getValue();
        }
        if (RATE_PAYMENT_CBM.equalsIgnoreCase(ratePayment)) {
            return CalculatePerEnum.CBM.getValue();
        }
        return CalculatePerEnum.CNTR.getValue();
    }

    private void saveCalLog(BlnoDO blnoDO, CalFrtReqVo calFrtReqVo, CalFrtRespVo calFrtRespVo,
        List<BlnoCntrDO> blnoCntrDOList, String calType, String status, String failMessage) {
        CalculateOperateLogCreateReqVO createReqVO = new CalculateOperateLogCreateReqVO();
        createReqVO.setBusinessId(blnoDO.getId());
        createReqVO.setBusinessNo(blnoDO.getBlNo());
        createReqVO.setPort(blnoDO.getCurrentPort());
        createReqVO.setCalculateType(calType);
        createReqVO.setOperationStatus(status);
        createReqVO.setOperationFailDesc(failMessage);
        Gson gson = new Gson();
        String reqJson = gson.toJson(calFrtReqVo);
        CalFrtRespDTO calFrtRespDTO = buildCalFrtRespDTO(calFrtRespVo);
        String rpsJson = gson.toJson(calFrtRespDTO);
        String cntrJson = gson.toJson(blnoCntrDOList);
        createReqVO.setReceiveJson(rpsJson);
        createReqVO.setSendJson(reqJson);
        createReqVO.setContainerJson(cntrJson);
        calLogService.createCalculateOperateLog(createReqVO);
    }

    private CalFrtRespDTO buildCalFrtRespDTO(CalFrtRespVo calFrtRespVo) {
        if (calFrtRespVo == null) {
            return null;
        }

        CalFrtRespDTO calFrtRespDTO = new CalFrtRespDTO();
        calFrtRespDTO.setQueryId(calFrtRespVo.getQueryId());
        calFrtRespDTO.setCalStart(calFrtRespVo.getCalStart());
        calFrtRespDTO.setCalEnd(calFrtRespVo.getCalEnd());
        calFrtRespDTO.setResultMsg(calFrtRespVo.getResultMsg());
        calFrtRespDTO.setResultCode(calFrtRespVo.getResultCode());
        List<CalFrtRateUnitRespDTO> rateUnits = new ArrayList<>();
        List<CalFrtRateUnitRespVo> rateUnitRespVoList = calFrtRespVo.getRateUnits();
        if (!CollectionUtils.isEmpty(rateUnitRespVoList)) {
            rateUnitRespVoList.forEach(vo -> {
                CalFrtRateUnitRespDTO dto = BlnoFreightConvert.INSTANCE.convert(vo);
                dto.setFeeDetails(vo.getFeeDetails());
                rateUnits.add(dto);
            });
        }

        calFrtRespDTO.setRateUnits(rateUnits);
        return calFrtRespDTO;
    }

    private CalFrtReqVo buildCalFrtReqVo(BlnoDO blnoDO, List<BlnoCntrDO> blnoCntrDOList, BlnoCargoDO blnoCargoDO,
        Map<Long, List<BlnoVslvoyDO>> vslvoyListMap, List<BlnoScnDO> scnList, Map<String, String> baseCodeNameMap) {
        CalFrtReqVo calFrtReqVo = BlnoFreightConvert.INSTANCE.convert(blnoDO);
        calFrtReqVo.setQueryId(IdUtil.getSnowflakeNextIdStr());
        String laneCode = VslvoyUtil.getFirstLaneCode(vslvoyListMap.get(blnoDO.getId()));
        calFrtReqVo.setLaneCode(laneCode);
        try {
            ImExVslvoyAge imExVslvoyAge =
                CostChargeUtil.getExVslvoyAge(vslvoyListMap.get(blnoDO.getId()), blnoDO.getCurrentPort());
            List<String> vesselVoys = new ArrayList<>();
            vesselVoys.add(imExVslvoyAge.getVesselCode());
            VesselVoyRespDTO voyRsp = costChargeUtil.getVoyQueryRespDTO(blnoDO.getCurrentPort(),
                imExVslvoyAge.getVoyage(), laneCode, vesselVoys);
            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
            String ptu = voyRsp.getPtu();
            calFrtReqVo.setBusinessDate(LocalDateTime.parse(ptu, formatter));
            calFrtReqVo.setScheduleId(voyRsp.getScheduleId());
        } catch (Exception e) {
            log.error("caught an exception on get businessDate", e);
            throw ExceptionUtil.exception(BlnoFreightErrorCodeConstants.BLNO_FREIGHT_ETU_IS_NULL, e.getMessage());
        }
        calFrtReqVo.setQueryDate(LocalDateTime.now());
        calFrtReqVo.setUserId(String.valueOf(SecurityFrameworkUtils.getLoginUserId()));
        scnList.forEach(blno -> {
            if (PartyTypeCodeEnum.SHIPPER.getValue().equals(blno.getPartyTypeCode())) {
                calFrtReqVo.setShipperCode(blno.getCsmCode());
            }
            if (PartyTypeCodeEnum.CONSIGNEE.getValue().equals(blno.getPartyTypeCode())) {
                calFrtReqVo.setConsigneeCode(blno.getCsmCode());
            }
            if (PartyTypeCodeEnum.NOTIFY.getValue().equals(blno.getPartyTypeCode())) {
                calFrtReqVo.setNotifyCode(blno.getCsmCode());
            }
        });
        calFrtReqVo.setShippingTermKey(blnoDO.getShippingTerm());
        calFrtReqVo.setShippingTermValue(baseCodeNameMap.getOrDefault(blnoDO.getShippingTerm(), null));
        calFrtReqVo.setFeePreferPayTerm(buildFeePreferPayMap(blnoDO.getId()));
        List<CalFrtRateUnitReqVo> rateUnitList = buildRateUnitList(blnoDO, blnoCntrDOList, blnoCargoDO);
        calFrtReqVo.setRateUnits(rateUnitList);
        return calFrtReqVo;
    }

    private Map<String, String> buildFeePreferPayMap(Long blnoId) {
        List<BlnoPaymentDO> paymentDOList = blnoPaymentService.getBlnoPaymentsByBlnoId(blnoId);
        return paymentDOList.stream().filter(Objects::nonNull).filter(payDo -> payDo.getChargeCode() != null)
            .collect(Collectors.toMap(BlnoPaymentDO::getChargeCode,
                payDo -> payDo.getDesignatedPayment() != null ? payDo.getDesignatedPayment() : "",
                (oldValue, newValue) -> oldValue));
    }

    private List<CalFrtRateUnitReqVo> buildRateUnitList(BlnoDO blnoDO, List<BlnoCntrDO> blnoCntrDOList,
        BlnoCargoDO blnoCargoDO) {
        List<CalFrtRateUnitReqVo> rateUnitList = new ArrayList<>();
        if (CollectionUtils.isEmpty(blnoCntrDOList)) {
            return rateUnitList;
        }

        List<BlnoCntrDO> blnoCntrDoDeDupList = deDuplicated(blnoCntrDOList);

        for (BlnoCntrDO cntr : blnoCntrDoDeDupList) {
            CalFrtRateUnitReqVo rateUnit = new CalFrtRateUnitReqVo();
            rateUnit.setCntrSizeType(cntr.getCntrSize() + cntr.getCntrType());
            rateUnit.setCargoType(blnoCargoDO.getCargoType());
            rateUnit.setCntrType(cntr.getCntrType());
            rateUnit.setCntrSize(Integer.valueOf(cntr.getCntrSize()));
            rateUnit.setCntrStatus(blnoDO.getCntrStatus());
            rateUnit.setCntrOwnerType(cntr.getCntrOwner());
            rateUnit.setHsCode(blnoCargoDO.getHsCode());
            rateUnit.setCargoName(blnoCargoDO.getCommodityEn());
            String oog = oogUtil.buildOog(cntr);
            rateUnit.setOog(oog);
            rateUnitList.add(rateUnit);
        }

        return rateUnitList;
    }

    private List<BlnoCntrDO> deDuplicated(List<BlnoCntrDO> list) {
        return list.stream()
            .collect(Collectors.collectingAndThen(
                Collectors.toMap(cntr -> Arrays.asList(cntr.getCntrSize(), cntr.getCntrType(), cntr.getCntrOwner()),
                    cntr -> cntr, (existing, replacement) -> existing),
                map -> new ArrayList<>(map.values())));
    }

}
